#pragma once

#include "alloc.h"

namespace sfc::boxed {

struct Drop {
  struct Meta {
    void (*drop)(void*);

    template <class T>
    Meta(trait::Type<T>) : drop{[](void* p) { static_cast<T*>(p)->~T(); }} {}
  };

  void drop() {
    (dyn::meta(this).drop)(dyn::self(this));
  }
};

template <class T>
class Box {
  T* _ptr = nullptr;

  explicit Box(T* ptr) noexcept : _ptr{ptr} {}

 public:
  Box() noexcept = default;

  explicit Box(T val) : _ptr{new T{static_cast<T&&>(val)}} {}

  Box(Box&& other) noexcept : _ptr{mem::take(other._ptr)} {}

  ~Box() {
    if (_ptr) {
      delete _ptr;
    }
  }

  Box& operator=(Box&& other) noexcept {
    auto tmp = static_cast<Box&&>(other);
    mem::swap(_ptr, tmp._ptr);
    return *this;
  }

  static auto from_raw(T* ptr) -> Box {
    return Box{ptr};
  }

  [[sfc_inline]] operator bool() const noexcept {
    return _ptr != nullptr;
  }

  [[sfc_inline]] auto ptr() const -> T* {
    return _ptr;
  }

  [[sfc_inline]] auto operator->() const -> const T* {
    panicking::assert_fmt(_ptr != nullptr, "boxed::Box::->: deref null");
    return _ptr;
  }

  [[sfc_inline]] auto operator->() -> T* {
    panicking::assert_fmt(_ptr != nullptr, "boxed::Box::->: deref null");
    return _ptr;
  }

  [[sfc_inline]] auto operator*() const -> const T& {
    panicking::assert_fmt(_ptr != nullptr, "boxed::Box::*: deref null");
    return *_ptr;
  }

  [[sfc_inline]] auto operator*() -> T& {
    panicking::assert_fmt(_ptr != nullptr, "boxed::Box::*: deref null");
    return *_ptr;
  }

  [[sfc_inline]] auto into_raw() && -> T* {
    return mem::take(_ptr);
  }
};

template <class T>
Box(T) -> Box<T>;

// DynBox
template <>
class Box<void> {
  dyn::Dyn<Drop> _dyn;

 public:
  template <class X>
  explicit Box(Box<X> box) : _dyn{*mem::move(box).into_raw()} {}

  ~Box() {
    if (_dyn.ptr() == nullptr) {
      return;
    }

    if (auto self = _dyn.operator->()) {
      (*_dyn.meta().drop)(self);
      delete (char*)(self);
    }
  }

  Box(Box&& other) noexcept : _dyn{other._dyn} {
    other._dyn = {};
  }

  operator bool() const noexcept {
    auto self = _dyn.operator->();
    return self != nullptr;
  }
};

// DynBox
template <class I>
class Box<I&> {
  dyn::Dyn<Drop, I> _dyn;

 public:
  explicit Box(auto val) : _dyn{Box<decltype(val)>{mem::move(val)}.into_raw()} {}

  ~Box() {
    if (auto self = _dyn.operator->()) {
      (*_dyn.meta().drop)(self);
      delete (char*)(self);
    }
  }

  Box(Box&& other) noexcept : _dyn{mem::take(other._dyn)} {}

  operator bool() const noexcept {
    return _dyn._self != nullptr;
  }

  auto operator->() {
    return _dyn.operator->();
  }

  auto operator->() const {
    return _dyn.operator->();
  }
};

// FnBox
template <class R, class... T>
class Box<R(T...)> {
  dyn::Dyn<Drop, dyn::Fx<R(T...)>> _dyn;

 public:
  Box() noexcept = default;

  template <class X>
  Box(X val) noexcept : _dyn{Box<X>{mem::move(val)}.into_raw()} {}

  ~Box() {}

  auto operator*() -> dyn::Fx<R(T...)>& {
    return *(_dyn.operator->());
  }
};

}  // namespace sfc::boxed
